package de.stefanocke.roo.wicket.base;

import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.propfiles.PropFileOperations;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.itd.AbstractItdMetadataProvider;
import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.metadata.MetadataItem;
import org.springframework.roo.model.DataType;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.process.manager.ProcessManager;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.support.logging.HandlerUtils;

import de.stefanocke.roo.wicket.ApplicationService;
import de.stefanocke.roo.wicket.MidUtils;
import de.stefanocke.roo.wicket.ValidationException;
import de.stefanocke.roo.wicket.base.html.HtmlBuilder;
import de.stefanocke.roo.wicket.base.html.HtmlBuilderRegistry;
import de.stefanocke.roo.wicket.registry.MetadataRegistry;

/**
 * Base class for all MetadataProviders.
 * Supports:
 * <p>
 * <li>Registration of trigger annotation</li>
 * <li>Generalized MID handling with MidUtils</li>
 * <li>Optional creation of AnnotationValues class</li>
 * <li>Stereotypes support (finding application-wide classes marked with
 * RooStereotype)</li>
 * </p>
 * 
 * @author stoc
 * 
 * @param <M>
 * @param <AV>
 */
@Component(componentAbstract = true)
public abstract class ItdMetadataProviderBase<M extends ItdMetadata, AV> extends AbstractItdMetadataProvider {

	protected static final Logger logger = HandlerUtils.getLogger(ItdMetadataProviderBase.class);
	static {
		logger.setLevel(Level.FINE);
	}

	public static JavaType javaType(String fqn, JavaType... parameters) {
		return new JavaType(fqn, 0, DataType.TYPE, null, Arrays.asList(parameters));
	}

	/**
	 * If there is an exception for some metadata, we don not report it again
	 * and again,
	 * but only once. For this, we remember the MIDs and the message here. They
	 * will be removed
	 * as soon as the metadata has been produced successfully.
	 */
	private static Map<String, Object> midsWithErrors = new HashMap<String, Object>();

	@Reference
	private PathResolver pathResolver;
	private MidUtils midUtils;
	private Class<?> trigger;
	private Class<M> metadataClass;
	private Class<AV> annotationValuesClass;
	@Reference
	protected ApplicationService applicationService;
	@Reference
	protected HtmlBuilderRegistry htmlBuilderRegistry;
	
    @Reference private ProcessManager processManager;

	@Reference
	private PropFileOperations propFileOperations;

	@Reference
	protected MetadataRegistry metadataRegistry;

	protected Class<?> metadataKind;

	public ItdMetadataProviderBase(Class<?> triggerClass, Class<M> metadataClass, Class<AV> annotationValuesClass) {
		this(triggerClass, metadataClass, annotationValuesClass, null);
	}

	public ItdMetadataProviderBase(Class<?> triggerClass, Class<M> metadataClass, Class<AV> annotationValuesClass,
			Class<?> kind) {
		super();
		this.metadataClass = metadataClass;
		this.midUtils = MidUtils.get(metadataClass);
		this.trigger = triggerClass;
		this.annotationValuesClass = annotationValuesClass;
		this.metadataKind = kind;
	}

	/**
	 * The activate method for this OSGi component, this will be called by the
	 * OSGi container upon bundle activation (result of the 'addon install'
	 * command)
	 * 
	 * @param context
	 *            the component context can be used to get access to the OSGi
	 *            container (ie find out if certain bundles are active)
	 */
	protected void activate(ComponentContext context) {
		metadataDependencyRegistry.addNotificationListener(this);
		metadataDependencyRegistry.registerDependency(PhysicalTypeIdentifier.getMetadataIdentiferType(),
				getProvidesType());
		Set<Class<?>> triggers = getTriggers();
		for (Class<?> trigger : triggers) {
			addMetadataTrigger(new JavaType(trigger.getName()));
		}

		metadataRegistry.registerMetadataClass(metadataKind, getMetadataClass(), triggers);

		registerHtmlBuilder();
	}

	/**
	 * The deactivate method for this OSGi component, this will be called by the
	 * OSGi container upon bundle deactivation (result of the 'addon uninstall'
	 * command)
	 * 
	 * @param context
	 *            the component context can be used to get access to the OSGi
	 *            container (ie find out if certain bundles are active)
	 */
	protected void deactivate(ComponentContext context) {
		metadataDependencyRegistry.removeNotificationListener(this);
		metadataDependencyRegistry.deregisterDependency(PhysicalTypeIdentifier.getMetadataIdentiferType(),
				getProvidesType());
		Set<Class<?>> triggers = getTriggers();
		for (Class<?> trigger : triggers) {
			removeMetadataTrigger(new JavaType(trigger.getName()));
		}
		metadataRegistry.deregisterMetadataClass(getMetadataClass());
		unregisterHtmlBuilder();
	}

	protected abstract ItdTypeDetailsProvidingMetadataItem getMetadataInternal(ItdMetadataCreationParams<AV> mcp);

	/**
	 * Return an instance of the Metadata
	 */
	protected ItdTypeDetailsProvidingMetadataItem getMetadata(String mid, JavaType aspectName,
			PhysicalTypeMetadata governorPhysicalTypeMetadata, String itdFilename) {
		try {
			if (governorPhysicalTypeMetadata.getMemberHoldingTypeDetails() == null) {
				// Some parser errors?
				return null;
			}
			Map<String, ClassOrInterfaceTypeDetails> stereotypes = findStereotypes(mid);
			AV annotationValues = createAnnotationValues(governorPhysicalTypeMetadata);
			ItdMetadataCreationParams<AV> metadataCreationParams //
			= new ItdMetadataCreationParams<AV>(mid, aspectName, governorPhysicalTypeMetadata, annotationValues,
					stereotypes);

			ItdTypeDetailsProvidingMetadataItem metadataItem = getMetadataInternal(metadataCreationParams);

			metadataRegistry.registerMetadata(metadataItem);

			updatePropertyFile(metadataItem);

			if (midsWithErrors.remove(mid) != null) {
				warning(governorPhysicalTypeMetadata, "Fixed problems with " + mid);
			}
			return metadataItem;
		} catch (ValidationException ve) {
			if (midsWithErrors.get(mid) == null) {
				if (!ve.isSilent()) {
					warning(governorPhysicalTypeMetadata, ve.getMessage());
				}
				midsWithErrors.put(mid, ve.getMessage());
			}
			return null;
		} catch (RuntimeException e) {
			String exceptionDescription = e.getClass() + " " + e.getMessage();
			if (midsWithErrors.get(mid) == null) {
				logStackTrace(logger, e);
				midsWithErrors.put(mid, exceptionDescription);
			}
			return null;

		}
	}

	private void updatePropertyFile(ItdTypeDetailsProvidingMetadataItem metadataItem) {
		if (metadataItem instanceof PropertyFileChanges) {
			PropertyFileChanges pfc = (PropertyFileChanges) metadataItem;

			Map<String, String> propertiesToAdd = pfc.getPropertiesToAdd();
			
			if(propertiesToAdd == null){
				return;
			}

			if (!propertiesToAdd.isEmpty()) {

				String mid = metadataItem.getId();

				String relativeFilename = PathUtil.getRelativeFilename(mid, "properties");
				LogicalPath logicalPath = PathUtil.getResourcesPath(mid);

				// TODO: Renaming support: Read the properties file at first, to
				// get
				// old values. ...

				propFileOperations.addProperties(logicalPath, relativeFilename, propertiesToAdd, true, false);
			}

		}

	}

	private static void logStackTrace(Logger logger, RuntimeException e) {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		PrintWriter s = new PrintWriter(baos);
		try {
			e.printStackTrace(s);
			s.flush();
			logger.log(Level.SEVERE, e.getMessage() + " " + baos.toString(), e);

		} finally {
			s.close();
		}
	}

	protected Set<Class<?>> getTriggers() {
		Set<Class<?>> classes = new HashSet<Class<?>>();
		classes.add(trigger);
		return classes;
	}

	protected Class<M> getMetadataClass() {
		return metadataClass;
	}

	protected void registerDependency(String upstreamDependency, String downStreamDependency) {
		if (metadataDependencyRegistry != null && StringUtils.isNotEmpty(upstreamDependency)
				&& StringUtils.isNotEmpty(downStreamDependency) && !upstreamDependency.equals(downStreamDependency)) {
			metadataDependencyRegistry.registerDependency(upstreamDependency, downStreamDependency);
		}
	}

	/**
	 * Define the unique ITD file name extension. By default, we use the
	 * metadata
	 * classname without the "Metadata"
	 */
	public String getItdUniquenessFilenameSuffix() {
		String suffix = getMetadataClass().getSimpleName();
		int pos = suffix.indexOf("Metadata");
		if (pos != -1) {
			suffix = suffix.substring(0, pos);
		}
		Validate.isTrue(suffix.length() > 3);
		return suffix;
	}

	protected MidUtils getMidUtils() {
		return midUtils;
	}

	protected String getGovernorPhysicalTypeIdentifier(String metadataIdentificationString) {
		JavaType javaType = getMidUtils().getJavaType(metadataIdentificationString);
		LogicalPath path = getMidUtils().getPath(metadataIdentificationString);
		return PhysicalTypeIdentifier.createIdentifier(javaType, path);
	}

	@Override
	protected String createLocalIdentifier(JavaType javaType, LogicalPath path) {
		return getMidUtils().createIdentifier(javaType, path);
	}

	protected static void validationError(String mid, String string) {
		validationError(mid, string, false);
	}

	protected static void validationError(String mid, String string, boolean silent) {
		throw new ValidationException(mid + ": " + string, silent);
	}

	protected void warning(PhysicalTypeMetadata governorPhysicalTypeMetadata, String msg) {
		// TODO: Remove "Managed" and improve message as soon as
		// https://issuetracker.springsource.com/browse/STS-1947 is fixed
		logger.warning("Managed "
				+ pathResolver.getFriendlyName(governorPhysicalTypeMetadata.getPhysicalLocationCanonicalPath()));
		logger.warning(msg);
	}

	protected static void validateType(String mid, String contextMessage, JavaType actual, JavaType... allowedTypes) {
		for (JavaType javaType : allowedTypes) {
			if (javaType.equals(actual)) {
				return;
			}
		}

		validationError(mid, contextMessage + ": Expected type is one of " + Arrays.toString(allowedTypes)
				+ " but actual type is " + actual.getNameIncludingTypeParameters());

	}

	protected static void validateMethodReturnType(String mid, MethodMetadata methodMetadata, JavaType... allowedTypes) {
		validateType(mid, "Return type of method " + methodMetadata.getMethodName(), methodMetadata.getReturnType(),
				allowedTypes);
	}

	@Override
	public String getProvidesType() {
		return getMidUtils().getMetadataIdentiferType();
	}

	protected AV createAnnotationValues(PhysicalTypeMetadata governorPhysicalTypeMetadata) {
		if (annotationValuesClass == null) {
			return null; // support cases where we don't need annotation values.
		}
		try {
			Constructor<AV> constructor = annotationValuesClass.getConstructor(PhysicalTypeMetadata.class);
			AV av = constructor.newInstance(governorPhysicalTypeMetadata);
			return av;
		} catch (Exception e) {
			throw new IllegalStateException("Exception while creating AnnotationValues for " + this, e);
		}
	}

	protected Map<String, ClassOrInterfaceTypeDetails> findStereotypes(String mid) {
		String[] required = requiredStereotypes();
		String[] optional = optionalStereotypes();
		if (required.length == 0 && optional.length == 0) {
			return Collections.emptyMap();
		}
		Map<String, ClassOrInterfaceTypeDetails> result = new HashMap<String, ClassOrInterfaceTypeDetails>();
		for (String s : required) {
			result.put(s, findStereotype(mid, s, true));
		}
		for (String s : optional) {
			ClassOrInterfaceTypeDetails type = findStereotype(mid, s, false);
			if (type != null) {
				result.put(s, type);
			}
		}
		return result;
	}

	private ClassOrInterfaceTypeDetails findStereotype(String mid, String stereotype, boolean required) {
		ClassOrInterfaceTypeDetails cid = applicationService.findClassOrInterfaceDetailsWithStereotype(getMidUtils()
				.getPath(mid).getModule(), stereotype);
		if (cid == null && required) {
			throw new ValidationException("No class found with annotation @RooStereotype(" + stereotype
					+ "). Please run wicket setup.");
		}
		if (cid != null) {
			// Diese Dependency bringt nicht allzuviel.
			// Ggf. sollte stattddessen hier auch die WicketMetadataRegistry
			// verwendet werden.
			registerDependency(cid.getDeclaredByMetadataId(), mid);
			return cid;
		} else {
			return null;
		}

	}

	/**
	 * 
	 * Metadata will be produced even if those stereotypes do not exist.
	 */
	protected String[] optionalStereotypes() {
		return new String[] {};
	}

	/**
	 * 
	 * If those stereotypes do not exist, it is an error.
	 */
	protected String[] requiredStereotypes() {
		if (requiredStereotype() != null) {
			return new String[] { requiredStereotype() };
		}
		return new String[] {};
	}

	protected String requiredStereotype() {
		return null;
	}

	protected Class<? extends HtmlBuilder<M>> getHtmlBuilderClass() {
		return null;
	}

	protected void registerHtmlBuilder() {
		Class<? extends HtmlBuilder<M>> htmlBuilderClass = getHtmlBuilderClass();
		if (htmlBuilderClass != null) {
			htmlBuilderRegistry.registerHtmlBuilder(getMetadataClass(), htmlBuilderClass);
		}
	}

	protected void unregisterHtmlBuilder() {
		Class<? extends HtmlBuilder<M>> htmlBuilderClass = getHtmlBuilderClass();
		if (htmlBuilderClass != null) {
			htmlBuilderRegistry.unregisterHtmlBuilder(getMetadataClass());
		}
	}

	/**
	 * Generic metadata listener, that uses {@link MetadataRegistry} to
	 * determine dependent Wicket Metadata
	 */
	@Override
	protected void notifyForGenericListener(String upstreamDependency) {

		if (MetadataIdentificationUtils.isIdentifyingClass(upstreamDependency)) {
			// It's just a class-specific notification (ie no instance), so we
			// don't care about it
			return;
		}

		// To get here we have an instance-specific identifier.
		// Let's try to grab the metadata
		MetadataItem metadataItem = metadataService.get(upstreamDependency);

		if (metadataItem == null || !metadataItem.isValid()
				|| !(metadataItem instanceof ItdTypeDetailsProvidingMetadataItem)) {
			// There's something wrong with it, so let's
			// gracefully abort
			return;
		}

		Set<String> mids = metadataRegistry.getDependentMetadata(metadataItem);
		// if(metadataItem.getId().startsWith("MID:de.stefanocke.roo")){
		// logger.warning("Dependent MIDs for "+metadataItem.getId()+" : "+mids);
		// }

		// Let's see if some of the dependent metadata is to be created by this
		// provider. If so, force re-creation of that metadata items.
		for (String mid : mids) {
			if (getMidUtils().isValid(mid)) {
				if(processManager.isDevelopmentMode()){
					logger.fine("MetadataProvider for " + getProvidesType()
					+" detected dependency from "+upstreamDependency+" to "+mid);
				}
				metadataService.evictAndGet(mid);
			}
		}

	}

}